<?php

	class comSpec{
		
		var $program;
		protected $rawdata, $opts, $io, $piped;

		function __construct($input=false){
			$this->io = new obj;
			if($input) $this->parse($input);
		}
		
		function parse($input){
			$this->rawdata = $this->get_rawdata($input);
			// parse IO
			$IO = $this->io;
			foreach($this->rawdata as $i=>$a){
				if($a['type'] != 'io') continue;
				$io = $a['value'];
				if($io == '|'){
					$comSpec = new comSpec;
					$comSpec->piped = true;
					$IO->set('|', $comSpec);
					$IO = $comSpec->io;
					continue;
				}
				$copyof = null;
				switch($io[0]){
					case '<':
						$mode = (substr($io, 1, 1) == '<') ? 'a' : 'w';
						$IO->set(0, array('mode'=>$mode, 'value'=>$this->pluck_rawvalue($i+1)));
						break;
					case '>';
						$mode = (substr($io, 1, 1) == '>') ? 'a' : 'w';
						$IO->set(1, array('mode'=>$mode, 'value'=>$this->pluck_rawvalue($i+1)));
						break;
					case '&';
						$mode = (substr($io, 2, 1) == '>') ? 'a' : 'w';
						$IO->set(1, array('mode'=>$mode, 'value'=>$this->pluck_rawvalue($i+1)));
						$IO->set(2, array('mode'=>$mode, 'value'=>'&1'));
						break;
					default:
						$stream = $io[0];
						if(strlen($io) > 2){
							if(substr($io, 2, 1) == '>'){
								$mode = 'a';
								if(substr($io, 4, 1)) $copyof = $io[4];
							}else{
								$mode = 'w';
								if(substr($io, 3, 1)) $copyof = $io[3];	
							}
						}else $mode = 'w';
						$IO->set($stream, array('mode'=>$mode, 'value'=>(is_null($copyof) ? $this->pluck_rawvalue($i+1) : '&'.$copyof)));
				}
				unset($this->rawdata[$i]);
			}
			// get progname and reset raw keys, passing on to children as we go
			$spec = $this;
			$rawdata = $this->rawdata;
			$this->rawdata = array();
			foreach($rawdata as $i=>$a){
				if($a['type'] == 'program'){
					$spec->program = $a['value'];
					continue;
				}
				if($a['type'] == 'io'){
					if($a['value'] == '|') $spec = $spec->io->get('|');
					continue;
				} 
				$spec->rawdata[] = $a;
			}
		}
	
		function rawdata_regex(){
			$pn = '(?<program>(?:^[^ \|\<\>]{1,})|((?<=\|)([^ \|\<\>]{1,})))';
			$dq = '(?:(?<=[ \=\>\<]{1})(?:\"(?<par_d>(?:(?!\\\").)+?)\"))';
			$sq = '(?:(?<=[ \=\>\<]{1})(?:\'(?<par_s>(?:(?!\\\').)+?)\'))';
			$stdio = '(?:\<|\>){1,2}'; // <,>,>>,<<
			$digio = "\d{1}$stdio"; // 1>, 0<, 2>, 1>> etc
			$rdtio = "(?:$digio\&\d{1})"; // 2>&1, 1>>&2, etc
			$bshio = '(?:\&\>)'; // &>
			$piped = '(?:\|)'; // |
			$io = "(?<io>$stdio|$rdtio|$digio|$bshio|$piped)";
			$so = '(?: [\-]{1}(?<shortopt>[^ \=]{1,}))';
			$lo = '(?: [\-]{2}(?<longopt>[^ \=]{1,}))';
			$np = "(?:(?<=[ \=\>\<]{1})(?<nonpar>[^ \<\>]{1,}))";
			return "/$pn|$dq|$sq|$io|$lo|$so|$np/";	
		}
	
		function get_rawdata($input){
			preg_match_all($this->rawdata_regex(), $input, $m);
			$matches = $m[0];
			$by_type = array();
			foreach($m as $k=>$v){
				if(is_numeric($k)) continue;
				$by_type[$k] = $v;
			}
			unset($m);
			$values = array();
			foreach($matches as $i=>$v){
				foreach($by_type as $k=>$a){
					if(!$a[$i]) continue;
					$values[] = array('type'=>$k, 'value'=>$a[$i]);
					break;
				}
			}
			return $values;
		}
		
		function pluck_rawvalue($i, $reset=false){
			if(!array_key_exists($i, $this->rawdata)) return;
			$a = $this->rawdata[$i];
			if(!$this->has_string_type($a)) return;
			if($reset) $this->remove_rawkey($i);
			else unset($this->rawdata[$i]);
			return $this->decode_value($a);
		}
		
		function remove_rawkey($i){
			 array_splice($this->rawdata, $i, 1);	
		}
		
		function has_string_type($a){
			return in_array($a['type'], array('par_d', 'par_s', 'nonpar'));
		}
	
		function getOpt($shortopts='', array $long=array()){
			// opts followed by single colon are required to have associated value
			// opts followed by double colon can optionally have associated value
			// other opts do not accept values
			// ...
			// parse given $shortopts
			$short = array();
			$n = strlen($shortopts);
			for($i=0; $i<$n; $i++){
				$c = $shortopts[$i];
				if($c == ':') continue;
				if($i+1 < $n){
					if($shortopts[$i+1] == ':'){
						if(($i+2 < $n)&&($shortopts[$i+2] == ':')){
							$short[] = array('o', $c);
							$i = $i+2;
							continue;
						}
						$i++;
						$short[] = array('r', $c);
						continue;
					}
				}
				$short[] = array('n', $c);
			}
			// search for short opts
			foreach($short as $opt){
				foreach($this->rawdata as $i=>$a){
					if($a['type'] != 'shortopt') continue;
					if($a['value'][0] != $opt[1]) continue;
					switch($opt[0]){
						case 'n':
							$this->rawdata[$i]['value'] = substr($a['value'], 1);
							if(!$this->rawdata[$i]['value']) $this->remove_rawkey($i);
							$this->opts[$opt[1]] = false;
							break;
						case 'o':
							$value = $this->pluck_rawvalue($i+1, 1);
							$this->opts[$opt[1]] = is_null($value) ? false : $value;
							break;
						case 'r':
							if($value = substr($a['value'], 1)){
								$this->opts[$opt[1]] = $value;
								$this->remove_rawkey($i);
								continue;
							}
							$value = $this->pluck_rawvalue($i+1, 1);
							if(is_null($value)) die("option {$opt[1]} requires a value");
							$this->opts[$opt[1]] = $value;
					}
				}	
			}
			// search for long opts
			foreach($long as $opt){
					switch(true){
						case (substr($opt, -2) == '::'):
							$opt = substr($opt, 0, -2);
							$t = 'o';
							break;
						case (substr($opt, -1) == ':'):
							$opt = substr($opt, 0, -1);
							$t = 'r';
							break;
						default:
							$t = 'n';
					}
					foreach($this->rawdata as $i=>$a){
						if($a['type'] != 'longopt') continue;
						if($a['value'] != $opt) continue;
						switch($t){
							case 'n':
								$this->opts[$opt] = false;
								break;
							case 'o':
								$value = $this->pluck_rawvalue($i+1, 1);
								$this->opts[$opt] = is_null($value) ? false : $value;
								break;
							case 'r':
								$value = $this->pluck_rawvalue($i+1, 1);
								if(is_null($value)) die("option $opt requires a value");
								$this->opts[$opt] = $value;
								break;						
						}
				}
			}
			// non-option args
			$this->opts[] = $this->program;
			foreach($this->rawdata as $i=>$v){
				if($this->has_string_type($v)) $this->opts[] = $this->decode_value($v);
			}
			return $this->opts;
		}
		
		function decode_value($a){
			// not yet
			return $a['value'];
			$p = '';
			switch($a['type']){
				case 'par_d':
					$p = '"';
					break;
				case 'par_s':
					$p = "'";
					break;
			}
			return $p.$a['value'].$p;
		}
	
		function io(){
			return $this->io;
		}
	
		function stream($n){
			if(!$s = $this->io->get($n)) return stdstream($n);
			switch(true){
				case is_string($s): //&n
					return $this->stream(substr($s, 1));
				case is_array($s):
					if(!$stream = sm_fopen($s['value'], $s['mode'])){
						// throw exception
						Transput()->inject("could not open {$s['value']}");
						exit;
						return false;
					}
					$this->io->set($n, $stream);
					return $stream;
				default:
					return $s;
			}
		}
		
		function close_streams(){
			foreach($this->io as $s) if($s instanceof Smuoi_Port) $s->close();	
		}
		
		function pipe(){
			return $this->io->get('|');	
		}
		
		function arg($n){
			return array_key_exists($n, $this->opts) ? $this->opts[$n] : null;	
		}
	
		function indexed(){
			$a = array();
			foreach($this->opts as $k=>$v) if(is_integer($k)) $a[] = $v;
			return $a;
		}
	
	}


